import * as CryptoJS from 'crypto-js';
import ThirtyTwo from 'thirty-two';

export enum Digest {
    SHA1,
    SHA256,
    SHA512
}


export class TOTP {
    protected key: string;
    protected digits: number;
    protected digest: Digest;
    protected interval: number;
    constructor(s:string, digits: number = 6, digest: Digest = Digest.SHA1, interval: number = 30) {
        this.key = s;
        this.digits = digits;
        this.digest = digest;
        this.interval = interval; 
    }
    
    hmac(data: Uint8Array) {
        const key = CryptoJS.lib.WordArray.create(ThirtyTwo.decode(this.key));
        const msg = CryptoJS.lib.WordArray.create(data);
        let hmac;
        switch (this.digest) {
            case Digest.SHA1:
                hmac= CryptoJS.HmacSHA1(msg,key);
                break;
            case Digest.SHA256:
                hmac= CryptoJS.HmacSHA256(msg,key);
                break;
            case Digest.SHA512:
                hmac= CryptoJS.HmacSHA512(msg,key);
                break;
        }

        const uint8Array = new Uint8Array(hmac.words.length * 4);
        for (let i = 0; i < hmac.words.length; i++) {
            const word = hmac.words[i];
            for (let j = 0; j < 4; j++) {
                uint8Array[i * 4 + j] = (word >> ((3 - j) * 8)) & 0xff;
            }
        }
        return uint8Array;
    }

    dynamicTruncation(hmacResult: Uint8Array) {
        const offset = hmacResult[hmacResult.length - 1] & 0x0f;
        return (
            ((hmacResult[offset] & 0x7f) << 24) |
            ((hmacResult[offset + 1] & 0xff) << 16) |
            ((hmacResult[offset + 2] & 0xff) << 8) |
            (hmacResult[offset + 3] & 0xff)
        );
    }

    at(seconds: number) {
        const timeCounter = Math.floor(seconds/ this.interval);
        const timeBytes = longToByteArray(timeCounter);
        const hmacResult = this.hmac(timeBytes);
        const binaryCode = this.dynamicTruncation(hmacResult);
        const otp = binaryCode % Math.pow(10, this.digits);

        let strOtp = String(otp);
        while (strOtp.length < this.digits) {
            strOtp = '0' + strOtp;
        }
        return strOtp;
    }

    now() {
        const current = new Date().getTime();
        return this.at(Math.floor(current / 1000));
    }
}

function longToByteArray(long: number) {
    const byteArray = new Uint8Array(8);
    for (let i = 7; i >= 0; i--) {
        byteArray[i] = long & 0xff;
        long = long >> 8;
    }
    return byteArray;
}


export interface TotpInfo {
    issuer?: string;
    accountName: string;
    secret: string;
    digits?: number;
    period?: number;
    algorithm?: string;
}

export function parseTotpUri(uri: string): TotpInfo {
    if (!uri.startsWith('otpauth://totp/')) {
        throw new Error('Invalid TOTP URI. It should start with otpauth://totp/.');
    }

    const pathAndQuery = uri.slice('otpauth://totp/'.length);
    const [path, queryString] = pathAndQuery.split('?');
    let issuer: string | undefined;
    let accountName: string;
    if (path.includes(':')) {
        [issuer, accountName] = path.split(':');
    } else {
        accountName = path;
    }

    const queryParams = parseQueryString(queryString);
    const secret = queryParams['secret'];
    if (!secret) {
        throw new Error('Missing "secret" parameter in TOTP URI.');
    }

    const digits = queryParams['digits']? parseInt(queryParams['digits'] as string, 10) : undefined;
    const period = queryParams['period']? parseInt(queryParams['period'] as string, 10) : undefined;
    const algorithm = queryParams['algorithm'];

    // 如果查询参数中有 issuer，优先使用查询参数中的 issuer
    if (queryParams['issuer']) {
        issuer = queryParams['issuer'] as string;
    }

    return {
        issuer,
        accountName,
        secret,
        digits,
        period,
        algorithm
    };
}

function parseQueryString(queryString: string) {
    const result = {};
    const pairs = queryString.split('&');
    for (const pair of pairs) {
        const [key, value] = pair.split('=');
        if (key) {
            result[decodeURIComponent(key)] = decodeURIComponent(value);
        }
    }
    return result;
}
